4. ÕPPEMATERJAL - funktsioonid ja objektid
funktsioonid ja objektid
| Õpikeskkond: | Tartu Ülikooli Moodle´i õpikeskkond |
| Kursus: | Teeme ise arvutimänge (MTAT.TK.008) |
| Raamat: | 4. ÕPPEMATERJAL - funktsioonid ja objektid |
| Printija: | Jaan Janno |
| Kuupäev: | neljapäev, 18. mai 2017, 12.51 |
RAAMAT 4


Kursuse "Teeme ise arvutimänge - algus"
4. RAAMAT
FUNKTSIOONID JA OBJEKTID
Tiina Kull
Tartu Ülikool
2012
Funktsioonid
Suurte programmide, nagu mängud tavaliselt on, kirjutamisel läheb kood suht kiiresti väga pikaks ja keeruliseks nii oma konstruktsioonide ja valemite poolest kui loetavuse enda mõttes. Sa oled omal nahal seda juba ussimängu koodi ümber kirjutades või oma seiklusmängu luues tunda saanud. Kood läheb aina pikemaks ja segasemaks.
Kõige parem oleks kood organiseerida väiksemateks juppideks, kus igast jupist on programmeerijal hea ülevaade, ta teab mida see jupp teeb ja milline tulemus selle jupi töötamisega kaasneb. Pealegi on ühte juppi kergem programmeerida kui kogu programmi korraga kirjutada.
Mis siis teha?
Võtsame kasutusele funktsiooni mõiste. Funktsioon ei olegi mitte midagi muud kui üks jupp koodi suurest programmist, millele on antud oma nimi ja mis oskab tagastada (välja anda) oma töö tulemust. Funktsioone nimetatakse sageli ka alamprogrammideks, sest nad käituvad nagu väikesed iseseisvad programmikesed. Funktsioon on käskude kogumik, mis teeb alati midagi. Funktsioone võib vaadata ka kui ehitusmaterjali maja ehitamisel. Iga materjal on oma otstarbega, oma spetsiifiliste omadustega, oma kujuga jne. Paljudest erinevatest ja ka ühesugustest materjali tükkidest saab oskuslikul kombineerimisel ehitada mistahes ehitisi. Sama lugu on funktsioonidega, neist saab oskuslikul kombineerimisel kokku panna mistahes programmi.
Funktsiooni loomine
Funktsioone luuakse ehk defineeritakse võtmesõna def abil. Def võtmesõna järele kirjutatakse funktsiooni nimi (nime valimisel tuleb lähtuda samadest reeglitest nagu muutujate nimede valimise juures) ning kõige lõpuks sulud. Sulgude sisse võib, aga ei pea midagi kirjutama, oleneb funktsiooni töö eripärast, kuid sellest veidi aja pärast. Praegu aga võta lahti Idle tekstiredaktor ja katseta funktsiooni defineerimist.

Defineerisin funktsiooni ahv.
See funktsioon ei tee mitte midagi muud, kui prindib neli rida teksti.
Funktsiooni tööle panemiseks tuleb funktsiooni nimepidi kutsuda, seda me
teeme antud näite viimasel real, kusjuures üliolulised on just sulud.
Vaata ka sissejuhatavat videot, mis toob väikese näite funktsiooni defineerimisest.
Kuid miks on veel kasulik eraldada grupp käske suurest koodist ja anda sellele käskude grupile ühine nimi? Kas tõesti ainult loetavuse parendamiseks?
Funktsiooni välja kutsumine
Vastan eelmise peatüki viimase rea küsimusele: ei, kindlasti ei ole funktsioonid ainult selleks, et pealkirjastada erinevaid koodijuppe umbes nagu pannakse raamatu peatükkidele pealkirju, ei. Funktsioonidel on palju, palju mõistlikum rakendus.
Tänu funktsioonidele võin ma kirjutada ühte koodijuppi ainult ühe korra ning kui mul on seda juppi vaja mitmes erinevas kohas, siis saan vaid viidata juba olemasolevale osale programmis. Seda võimaldab funktsiooni nimi. Võin funktsiooni välja kutsuda mistahes teises programmi osas ja nii mitu korda kui vaja, ilma et oleks pidanud uuesti ja uuesti sama koodijuppi kirjutama.
Vaatame ühte lihtsat näidet:

Siin näites ma defineerin kaks funktsiooni, kiitus ja laitus.
Need funktsioonid ma kutsun välja kahes erinevas if-lauses, kord kahe
arvu summa kontrolli juures, kord kahe arvu vahe kontrolli juures. Kuigi
mu funktsioonid on väga väikesed ja ei tee suurt midagi, on aja
kokkuhoid aga juba märgatav.
Jah, ma oleksin võinud ka need paar lauset kopeerida mõlemasse if-lausesse, kuid see muudab koodi kirjumaks ja loetamatumaks. Pealegi üldjuhul on funktsioonide sisu märksa keerulisem, kui lihtsalt print käsk.
Veel
üks oluline punkt, miks kasutada funktsioone. Kui ma pean mitu korda
ühte ja seda sama koodijuppi mitmes eri kohas kasutama, oletame et ma
olen juba need jõudnud ära kirjutada ja ma ei kasuta funktsioone. Mingi
aja pärast aga avastan, et olen selles programmijupis teinud olulise
vea, siis nüüd pean vea parandamiseks terve programmi uuesti läbi käima
(ütleme nii 10-100 kohas), siis on see üks väga tüütu asi. Funktsioonide
korral oleks mul piisanud vaid funktsiooni sees ühe korra muutus teha
ja see oleks funktsiooni nime kaudu kui viida kaudu kõikidesse
väljakutsutavatesse kohtadesse mõjunud.
Funktsiooni argumendid
Funktsiooni argument on info, mida saame funktsioonile anda ja see kirjutatakse funktsiooni sulgude sisse. Sellist info funktsioonile andmist nimetatakse funktsioonile argumentide andmiseks.
Mõistete veel rohkem segamini ajamiseks kutsuvad osad programmeerijad funktsiooni sulgudesse lisatud infot ka parameetriks. Suurt vahet neil kahel mõistel tegelikult ei ole. Erinevus tuleb inglisekeelsete sõnade kasutamise eripärast. Seega kui ma annan info funktsioonile tema väljakutsumise hetkel, siis kutsutakse seda infot argumendiks ning kui ma kasutan saadud infot funktsiooni sees millegi tegemiseks, siis parameetriks.
Vaatame seda sama arvutamise näidet, aga nüüd koos argumendiga funktsioonis.

Näiteprogrammis muutus see, et funktsioon kasutab oma töös nüüd ka talle antud infot (argumenti). Argumenti vastus kasutatakse funktsiooni sees print käsus.
Argument antakse funktsioonile tema väljakutsumise hetkel.
Antud näites on funktsiooni definitsioonis ja funktsiooni väljakutsumisel kasutatud ühte ja seda sama muutuja nime vastus. Kuid soovitav on seda mitte teha. Muutujanimed (ka argumendi nimed) võivad olla funktsiooni sees täiesti erinevad nendest, mida kasutatakse funktsiooni väljakutsumise ajal.
Argumente võib funktsioonil olla ka mitmeid, igale argumendile tuleb anda oma nimi. Funktsiooni väljakutsumisel on oluline väärtuste järjekord, mis sulgude sisse kirjutatakse.
Vaatame ka videot, kus on nüüd juba veidi keerulisema funktsiooni defineerimise näide, kus on ka argumendid.
Funktsiooni töö tulemuse tagastamine
Praegu oleme näitena kasutanud selliseid funktsioone, mis kohe midagi väljastavad print() käsu abil. See ei ole aga tavaline funktsioonide kasutamine. Sageli kasutatakse funktsioone millegi välja arvutamiseks, mingi objekti muutmiseks, millegi lisamiseks ja välja trükkimisega ei ole neil suurt midagi pistmist. Kui aga funktsioon teeb mingi muutuse või arvutuse, kuidas ma muutusest teada saan, kuidas ma arvutuse kätte saan?
Esiteks võid vaadata videost, kuidas see käib:
Funktsiooni töö tulemuse tagastamine
Selleks kasutatakse funktsiooni sees võtmesõna return. Return sõna taha tuleb kirjutada tulemus, mis tahetakse tagastada. Katseta kindlasti järgmist näidet:

Antud näites viimase print() käsu sees kutsutakse silindri ruumala funktsioon välja. Pane tähele, et funktsiooni argmentideks anti kasutajalt küsitud kõrgus ja raadius. Funktsiooni sees on aga argumentide nimedeks ehk parameetriteks hoopis h ja r. Funktsiooni ülesanne on ruumala välja arvutada ja anda tagasi ruumala tulemus return käsu abil. Seega ei prindita print käsuga välja funktsiooni nimi või tema sisu vaid arvutatud silindri ruumala.
Lokaalsed ja globaalsed muutujad

Antud näites on oluline tähele panna erinevaid muutuja nimesid funktsiooni sees ja väljaspool funktsiooni, kuigi tähendavad need muutujad ju mõlemal korral silindri kõrgust ja silindri raadiust. Siin tulevadki mängu mõisted lokaalne ja globaalne muutuja.
Lokaalseks muutujaks nimetakse muutujat, mis asub funktsiooni sees. Selles näites siis muutujad h, r ja ruumala on lokaalsed. Muutujad kõrgus ja raadius on aga globaalsed. Mis neil vahet on, miks kutsutakse ühte lokaalseks aga teist globaalseks?
Enne kui lähen täpse selgituse juurde, räägin sulle veidi taustast. Muutujate loomine Pythonis on seotud mälu haldamisega. Nimelt ei ole Pythonis vaja mälu peale selle kasutusele võtmist uuesti vabastada, seda teeb Python meie eest ise. Mitmes teises programmeerimiskeeles (C, C++) on vaja muutujate poolt hõivatud mälu ise vabastada. Kui seda ei tehta, võib mälu suht kiiresti täis saada ja arvuti muutub siga-aeglaseks.
Lokaalse ja globaalse muutuja vahe seisneb nende muutujate mõjuala suuruses. Pythonis on kõik funktsioonide sees olevad muutujad lokaalsed muutujad ja neid muutujaid ei saa väljaspool funktsiooni kasutada. Kui ma näiteks kirjutaksin programmi lõppu veel ühe print rea ja tema sulgudesse kirjutaksin funktsioonimuutuja ruumala, siis saaksin veateate, mis ütleb, et programm sellist muutujat ei tunne. Pythonis on nii korraldatud, et enne funktsiooni tööle hakkamist ei eksisteeri tegelikult funktsiooni sees kasutatavad muutujad. Alles siis kui funktsioon välja kutsutakse tekitab Python vastavad muutujad (lokaalsed), antud juhul h, r ja ruumala ning omistab neile seal samas väärtused. h saab väärtuse argumendilt kõrgus ja r saab väärtuse argumendilt raadius. Ruumala leitakse muutujate pi, h ja r korrutamise teel. Kohe kui funktsioon on tulemuse väljakutsujale ehk ruumala küsijale tagastanud, kustutab Python kõik funktsioonis kasutusel olnud muutujad - mälu vabastatakse ja nii muutuvadki funktsiooni muutujad programmi jaoks taas olematuks.
Globaalsed muutujad seevastu eksisteerivad kogu programmi töö vältel ja neid saab kasutada igal pool, ka funktsioonide sees.
Kui
mujal programmi koodis saab globaalset muutujat iga kell muuta siis
funktsiooni sees seda niisama teha ei saa, saab vaid kasutada tema
väärtust. Miks nii? Kohe kui ma tahaksin näiteks funktsiooni sees
globaalsele muutujale raadius anda uue väärtuse, näiteks nii raadius=20,
siis tegelikult selline rida globaalset muutujat ei muuda, sest nagu ma
ennist ütlesin, kõik muutujad, mis asuvad funktsiooni sees, luuakse
uuesti, isegi kui sellel on sama nimi mis globaalsel muutujal. Sellises
olukorras luuakse lihtsalt uus globaalse muutujaga samanimeline kuid
lokaalne muutuja.
Globaalse muutuja muutmine funktsiooni sees
Kui mul on aga vaja ikkagi mõne funktsiooni sees muuta globaalset muutujat, siis tuleb globaalse muutuja nime ette kirjutada global, sellisel juhul muudab funktsioon tõesti globaalset muutujat ja ei tee valmis ajutise samanimelise lokaalse muutuja.
Pikk jutt räägitud, vaatame kuidas see muutujate asi ka reaalselt programmeerimise juures välja näeb.
Objektid
Juba
teist nädalat vaatame erinevaid võimalusi kuidas organiseerida andmeid
ja koodilõike oma programmis. Sarnaseid muutujaid saime ühte punti
siduda listide abil, koodijuppe funktsioonide abil.
Objektide mängu toomine on aga samm veel kaugemale. Objektide abil saab ühise nime alla liigitada nii funktsioone kui muutujaid koos. Objektid on programmeerimise maastikul väga levinud idee ja seda kasutatakse väga paljudes erinevates programmeerimiskeeltes. Nii et kui oled ühe korra ideest aru saanud, saad hakkama mistahes teise objekt-orienteeritud programmeerimiskeelega samuti.
Miks objektidest rääkida?
Kui me nüüd objektide teemaga edasi läheme, siis mõistad, miks Pythonit nimetatakse objekt-orienteeritud keeleks - siin on kõik asjad objektide alla kuuluvad, kuigi kohe alguses ei pruugi sellest arugi saada. Siiamaani polegi meie ülesannetes ühtegi kohe arusaadavat objekti ette jäänud, kuid järgmine nädal hakkame põhjalikumalt tegelema graafikaga, siis ilma objekti mõiste tundmiseta nii kergesti hakkama ei saa. Seega olgu antud peatükk sissejuhatuseks järgmisesse nädalasse.
Mis on objekt?
Võtame ühe konkreetse lihtsa näite: raamat. Me võime raamatut vaadata kui objekti. Mida see tähendab?
Me
saame objektiga raamat teha iga kord mingeid tegevusi - näiteks lugeda,
kinkida, riiulisse panna, kinni panna, lahti teha jne.- Me saame kirjeldada selle objekti omadusi - lehekülgede arv, paksud või õhukesed kaaned, mõõtmed, kuju, kirjastus, pealkiri, žanr jne.
Programmeerimises tehakse objektidega täpselt samu asju - tegevused, mida objektiga saab teha ja objekti kirjeldus. Pythonis kutsutakse andmeid, mida sa objekti kohta tead, atribuutideks ning tegevusi, mida sa objektiga saad teha, meetoditeks.
Objekt = atribuudid + meetodid
Näiteks kui me oleksime Pythonis loonud objekti raamat, siis raamatul võiksid olla
- sellised atribuudid: raamat.lk_arv, raamat.suurus, raamat.kuju jne.
- ning sellised meetodid: raamat.lugemine(), raamat.avamine(), raamat.sulgemine() jne.
Panid tähele erinevust ja seda punkti kasutamise asja? Tuleta meelde, me oleme ju objektidest teadmata neid võtteid näidetes kasutanud küll ja küll. Näiteks ajamõõtmise programm, kus kasutasime time.sleep()-i või listidesse andmete juurde panemise juures linnad.append(). Seega nii time kui linnad on Pythoni jaoks objektid ja punktiga taga olev käsk selle objekti meetod.
Atribuutideks saavad olla kõiksugused muutujad ja nende loetelud (arvud, kirjeldused, listid jne) ning meetoditeks on tavalised funktsioonid ja need kuuluvad ainult sellele konkreetsele objektile. Atribuudid on informatsioon objekti kohta ja meetodid on tegevus objektiga.
Atribuudid ei ole mitte mingil moel teistsugused kui tavalised muutujad, nendele omistatakse väärtuseid täpselt sama moodi ja neid kasutatakse koodis täpselt sama moodi kui tavalisi muutujaid, millega me juba harjunud oleme. Ainuke erinevus on selles, et atribuuti kasutatakse alati objekti nimega koos ja need on ühendatud punktiga.
Objekti loomine
Objekti loomine käib kahes etapis:
- Kõigepealt tuleb defineerida objekti juurde kuuluvad atribuudid ja meetodid (seda punkti võib võrrelda plaani koostamisega maja ehitamisel. Tegelikku maja veel ei ole, kuid joonis on juba valmis). Sellist plaani koostamist Pythonis kutsutakse klassiks.
- Teise sammuna tuleb võtta loodud klass (ehk plaan) ja konstrueerida selle järgi päris objekt (nagu plaani järgi maja ehitamine). Klassi järgi võib luua mitmeid ühesuguseid objekte nagu ka ühe ja sama plaani järgi võib luua mitmeid ühesuguseid maju. Klassist loodud objekte nimetatakse programmeerimises klassi isenditeks.
Uurime ühe klassi ja isendi loomist ning proovi ka ise ühe klassi tegemisega hakkama saada.

Kõrval
olevas näites tegin ühe lihtsa klassi raamat, kus on kaks meetodit,
avamine ja sulgemine, mis peaksid muutma objekti pildi ekraanil kas
lahti olevaks raamatuks või kinni olevaks raamatuks.
Kuhu jäid atribuudid?
Atribuudid ei kuulu klassi juurde vaid iga objekti ehk isendi juurde eraldi. Seetõttu saab iga tüüpplaani järgi ehitatud maja olla ka erinevat värvi või erineva fassaadimaterjaliga. Iga objekt saab omada iseendale omast kirjeldust, kuid käituvad kõik ühe klassi isendid alati ühte moodi.
Objekti isendi loomine
Nagu
üleval jutuks oli, siis klassi loomine on vaid plaani tegemine, nüüd
tuleb ehitama hakata. Et tekitada isendit, tuleb programmis isend luua
ja seda tehakse nii:

Et asi veel paremini selgeks saada, vaatame ka videot alternatiivse näitega ja kordame asja üle. 
Esmalt vaatame näidet, milles on välja toodud, kuidas objektidele atribuute määrata.
Seejärel uurime ka meetodi lisamist klassile.
Objekti algväärtustamine
Klassi
kirjeldusse atribuute otse ei kirjutata, kuid hirmus tüütu oleks ikkagi
iga objekti loomisel talle järjest argumente omistada. Selle mure saab
lahendada spetsiaalse meetodi __init__() abil. Klassi luuakse vastav meetod __init__() ning selle sees on võimalik anda igale loodavale isendile nö algväärtused, algparameetrid. __init__()
on spetsiaalne meetod (nii ees kui taga on kaks alakriipsu), mis
käivitub alati, kui uus objekt luuakse, andes seeläbi objektile ka
algväärtused ehk esmase kirjelduse. init tuleb inglisekeelsest sõnast initializing, mis tähendabki algväärtuseid andma.
Muudame raamatu näidet nii, et ei peaks eraldi objekti iseloomustama hakkama:


Muudatuse tulemusel programmi töö ei muutunud absoluutselt, kuid pääseme vaevast igale objektile eraldi algväärtuseid andmast, sest saame need kohe objekti loomisel parameetritena sulgude sisse kirjutada.
Proovime sama initsialiseerimist läbi teha ka eelmistes videotes alustatud näitega.
self
Igal pool klassi meetodites on kasutatud sõna self, mida see teeb, mis see on?
Self on üks kaval sõna
Asi on nimelt selles, et klassi defineerimine tähendab ainult plaani
või joonise tegemist, mille järgi objekte tegema hakatakse. Objekte aga
võib olla palju ühesuguseid ainult erineva nimega. Siit tulebki välja
vajadus isevärki sõna järele.
Kuidas?
Kui meil on mitu objekti raamatuid, näiteks jutukas, lastekas, romaan vms., siis iga selle objekti jaoks kehtivad täpselt ühed ja samad meetodid, mis on kirjeldatud klassis. Kust programm peab teadma meetodi käima panemisel, millise isendi parameetreid ta peaks muutma? Sellepärast ongi võetud kasutusele lisamuutuja self, mis saab väärtuse objektilt, kes ta välja kutsus. Kui kutsuja on jutukas.avamine(), siis self=jutukas, kui romaan.sulgemine(), siis self=romaan jne.
Muideks muutuja nime self asemel võib tegelikult kasutada mistahes sõna, kuid jällegi, tasub hoida kinni traditsioonidest, sest see muudab teiste koodi lugemise ja sinu koodi lugemise palju arusaadavamaks.
Miks on objektid head?
Lisaks sellele, et ma saan klassifitseerida erinevaid funktsioone ja muutujaid, siis on veel paar head asja, milleta programmeerijad enda elu ettegi ei kujuta.
Sellest järgnevast aru saamine pole küll kohustuslik ning ka ülesanded seda ei sisalda, aga huvi korral võid läbi lugeda. 
Esimene neist on polümorfism
Möh ...?
Kole mõiste, aga lihtne seletus. Ma saan kasutada täpselt sama funktsiooni nime erinevate klasside koosseisus. Oletame, et ma olen valmis teinud programmi erinevate geomeetriliste kujundite ruumalade arvutamiseks. Selles programmis on iga erineva kujundi jaoks defineeritud eraldi klass, mis on ju loogiline, igal kujundil on erinevad omadused, pindalade ja ruumalade arvutamise valemid. Kuid igas klassis saan ma kasutada ühte ja seda sama meetodi nime ruumala(), sest ta kuulub erinevatesse klassidesse. Segamini ei saa need meetodid kuidagi minna, sest neid meetodeid rakendatakse vaid oma klassi objektidele. Näiteks: silinder.ruumala(), püramiid.ruumala(), kuup.ruumala() jne., kujundi nimi enne punkti ütleb, millisest klassist ruumala() meetod tuleb välja kutsuda ja seetõttu arvutataksegi igale kujundile just temale sobilik ruumala.
Teine on aga päritavus
Objekt-orienteeritud programmeerimises saab defineerida nö alamklasse. Mis see annab? See annab selle, et kõige kõrgemas klassis on mul kirjas ainult need meetodid, mis kuuluvad kõikidele selle klassi objektidele, alamklassidesse aga saan ma juurde kirjutada spetsiifilisemaid meetodeid, mis kõikidel objektidel puuduvad. Näiteks:
Tihti lugu mängudes tuleb koguda igasuguseid esemeid: palle, linde, mune, münte jne. Kõik need esemed võivad kuuluda ühte suurde klassi MänguObjektid, millel on atribuut nimi (muna, münt) ja meetod korjaÜles(). Kuid sageli on osad esemed mängudes väärtuslikumad kui teised. Näiteks mündid, nendega saab edaspidi kindlasti mingi tehingu teha, seega müntide jaoks tuleks teha alamklass Münt, mis pärib kõik meetodid oma ülemuselt, kuid lisaks saame talle juurde kirjutada meetodi kuluta(), mis aitab rahaga tehinguid teha.

Näide
Vaatame videos alustatud klassi veelgi edasi.
Proovime nüüd luua juba mitu eksemplari ühe klassi objektidest, paneme neid ka listi ning töötleme tsükliga.
Mida õppisid?
Selle
nädalaga sai vast tiir peale tehtud kõigele vajalikule, et järgmine
nädal asuda graafika kallale, et juba vingemaid mänge tegama saaks
hakata. See nädal õppisid:
- funktsioonide loomist
- funktsioonide kasutamist
- funktsioonide välja kutsumist
- kuidas anda funktsioonile infot (argumente)?
- kuidas saada funktsiooni käest töö tulemus tagasi?
- mis vahe on lokaalsetel ja globaalsetel muutujatel?
- kuidas globaalset muutujat funktsiooni sees muuta?
- mis on objektid?
- kuidas objekte luuakse?
- mis asjad on atribuudid ja meetodid?
- mis asi on klass?
- klassi defineerima
- mis nähtus on polümorfism?
- kuidas luua alamklasse?